Joel Denke
08/04/2025, 3:13 PMmbonnin
08/04/2025, 3:29 PMmbonnin
08/04/2025, 3:29 PMJoel Denke
08/04/2025, 5:13 PMJoel Denke
08/04/2025, 5:14 PMJoel Denke
08/04/2025, 5:15 PMsimon.vergauwen
08/05/2025, 7:28 AMJust deploy directly to GCP or something.To easily deploy Ktor to GCP use the Ktor Gradle Plugin. It can upload directly to Google's Container Registry, which you can then easily deploy into Google Cloud. An example. The official Ktor samples has an example of a reverse proxy build in Ktor, so you could clone and adjust it a bit to your needs. https://github.com/ktorio/ktor-samples/tree/main/reverse-proxy. By combining those two it should be fairly easy even without AI βΊοΈ Let me know if you have any issues or questions, or checkout the #C0A974TJ9 channel for Ktor specific issues.
Joel Denke
08/05/2025, 7:52 AMmbonnin
08/05/2025, 8:07 AMGoogle App EngineI would forget about App Engine. I wouldn't be surprised it'd be deprecated in favor of Cloud Run soon
simon.vergauwen
08/05/2025, 8:08 AMFor new Google Cloud users, we recommend using Cloud Run as the preferred alternative over App Engine.Yes, I was just going to say. Cloud Run is the new kid on the block, so I would go with that. It's almost 1-click to deploy if your app is already in the Google Container Registry.
mbonnin
08/05/2025, 8:11 AMmbonnin
08/05/2025, 8:11 AMmbonnin
08/05/2025, 8:12 AMJoel Denke
08/05/2025, 12:58 PMJoel Denke
08/05/2025, 12:59 PMJoel Denke
08/05/2025, 1:01 PMmbonnin
08/05/2025, 1:43 PMmbonnin
08/05/2025, 1:46 PMmbonnin
08/05/2025, 1:47 PMlatest
tag)? Do you have a favorite workflow there?mbonnin
08/05/2025, 1:48 PMsimon.vergauwen
08/05/2025, 2:19 PMWhile we're on this topic, do any of you have an easy way to deploy a new revision of an image? I use the Cloud Run Java SDK to create a new revision where I tweak the revision name but this feels a bit awkward...Yes, I publish it from CI using Gradle using the Ktor Plugin. Most companies I worked at we used a combination of SHA + semantic versioning of the application. SHA alone should be sufficient but they're hard to read and reason. That in combination with staged rollouts is a pretty nice workflow. @mbonnin I think your
BuildImageTask
can be replaced by the Ktor plugin. It also uses JIB under the hood. I see you're on Ktor 2.x.x, so maybe the plugin didn't exist at that time? π€mbonnin
08/05/2025, 2:23 PMktor
plugin π
mbonnin
08/05/2025, 2:24 PMmbonnin
08/05/2025, 2:24 PMmbonnin
08/05/2025, 2:26 PMsimon.vergauwen
08/05/2025, 2:26 PMsimon.vergauwen
08/05/2025, 2:27 PMJoel Denke
08/06/2025, 7:08 AMmbonnin
08/06/2025, 7:30 AMJoel Denke
08/06/2025, 7:31 AMJib.from("openjdk:17-alpine")
.addLayer(listOf(path), AbsoluteUnixPath.get("/"))
.addLayer(runtimeClasspath.files.map { it.toPath() }, AbsoluteUnixPath.get("/classpath"))
.setEntrypoint(
"java",
"-cp",
(runtimeClasspath.files.map { "classpath/${it.name}" } + path.name).joinToString(":"),
mainClass.get())
.containerize(containerizer)
Not even sure if that what I want, but testing at the moment πmbonnin
08/06/2025, 7:32 AMJoel Denke
08/06/2025, 7:32 AMJoel Denke
08/06/2025, 7:33 AMJoel Denke
08/06/2025, 7:34 AMktor {
docker {
jreVersion.set(JreVersion.JRE_21)
localImageName.set("sample-docker-image")
imageTag.set("0.0.1-preview")
externalRegistry.set(
DockerImageRegistry.dockerHub(
appName = provider { "ktor-app" },
username = providers.environmentVariable("DOCKER_HUB_USERNAME"),
password = providers.environmentVariable("DOCKER_HUB_PASSWORD")
)
)
}
}
Joel Denke
08/06/2025, 7:35 AMmbonnin
08/06/2025, 7:35 AMdocker
terminology sounds a bit suspicious there. jib != dockermbonnin
08/06/2025, 7:35 AMJoel Denke
08/06/2025, 7:35 AMsimon.vergauwen
08/06/2025, 7:36 AMmbonnin
08/06/2025, 7:36 AMJoel Denke
08/06/2025, 7:37 AMsimon.vergauwen
08/06/2025, 7:38 AM@simon.vergauwen why does ktor say "docker" if it's using Jib?Jib builds optimized Docker and OCI images for your Java applications without a Docker daemon - and without deep mastery of Docker best-practices. Jib is still docker π
mbonnin
08/06/2025, 7:38 AMJoel Denke
08/06/2025, 7:38 AMmbonnin
08/06/2025, 7:38 AMmbonnin
08/06/2025, 7:38 AMJoel Denke
08/06/2025, 7:39 AMmbonnin
08/06/2025, 7:39 AMmbonnin
08/06/2025, 7:39 AMsimon.vergauwen
08/06/2025, 7:39 AMJoel Denke
08/06/2025, 7:40 AMmbonnin
08/06/2025, 7:40 AMdocker {}
by ociImage {}
then to avoid conflating the brand and the technologysimon.vergauwen
08/06/2025, 7:41 AMmbonnin
08/06/2025, 7:41 AMdocker {}
. Sorry if I'm bringing confusion here by nitpicking the namembonnin
08/06/2025, 7:42 AMJoel Denke
08/06/2025, 7:42 AMktor {
docker {
localImageName = "ktor-ai-example"
imageTag = project.version.toString()
externalRegistry =
googleContainerRegistry(
projectName = provider { "Kotlin Conf 2025" },
appName = provider { project.name },
username = providers.environmentVariable("GCLOUD_REGISTRY_USERNAME"),
password = providers.environmentVariable("GCLOUD_REGISTRY_PASSWORD"),
)
}
fatJar {
allowZip64 = true
archiveFileName.set(project.name)
}
}
mbonnin
08/06/2025, 7:43 AMJoel Denke
08/06/2025, 7:45 AMmbonnin
08/06/2025, 7:52 AMJoel Denke
08/06/2025, 7:53 AMJoel Denke
08/06/2025, 7:54 AMsimon.vergauwen
08/06/2025, 7:56 AMJoel Denke
08/06/2025, 7:57 AMmbonnin
08/06/2025, 7:59 AMmbonnin
08/06/2025, 8:00 AMmbonnin
08/06/2025, 8:00 AMmbonnin
08/06/2025, 8:01 AMsimon.vergauwen
08/06/2025, 8:01 AMnative could be interesting if your load is very unevenI think GraalVM Netty would outperform Kotlin/Native with CIO due to Netty being more efficient but that'd be a super interesting test actually π
Joel Denke
08/06/2025, 8:02 AM[bundles]
ktor = [
"ktor-json",
"ktor-logging",
"ktor-content-negotiation"
]
ktor-server = [
"ktor-server-sessions",
"ktor-server-core",
"ktor-server-cors",
"ktor-server-netty"
]
google-cloud = [
"google-cloud-datastore",
"google-cloud-run",
"google-cloud-storage",
"google-cloud-tools-jib-core"
]
This kind of setup I wish was predefinied somewhere for stuff, not only BOM but also predefinied constants of all combinations. I did see some libs doing this, like Koin πsimon.vergauwen
08/06/2025, 8:03 AMJoel Denke
08/06/2025, 8:03 AMJoel Denke
08/06/2025, 8:04 AMversionCatalogs {
create("ktorLibs") {
from("io.ktor:ktor-version-catalog:3.2.0")
}
}
Joel Denke
08/06/2025, 8:05 AMJoel Denke
08/06/2025, 8:05 AMcreate("ktorLibs") {
from(libs.ktorVersionCatalor)
}
Joel Denke
08/06/2025, 8:09 AMdependencies {
implementation(libs.bundles.ktor.server)
}
Ah well I go with this for now, and will see how can improve later on, with nesting version catalogs with custom bundle point to another version catalog πJoel Denke
08/06/2025, 8:16 AMimport io.ktor.plugin.features.DockerImageRegistry
plugins {
alias(libs.plugins.ktor)
kotlin("jvm")
}
application.mainClass = "sample.ApplicationKt"
ktor {
docker {
jreVersion = JavaVersion.VERSION_17
localImageName = "sample-docker-image"
imageTag =project.version.toString()
externalRegistry = DockerImageRegistry.googleContainerRegistry(
projectName = provider { "Project" },
appName = provider { project.name },
username = providers.environmentVariable("GCLOUD_REGISTRY_USERNAME"),
password = providers.environmentVariable("GCLOUD_REGISTRY_PASSWORD"),
)
fatJar {
allowZip64 = true
archiveFileName.set(project.name)
}
}
}
dependencies {
implementation(libs.bundles.ktor.server)
}
mbonnin
08/06/2025, 8:19 AMdocker
even when running Jib (which kind of makes my naming point above numb) Nope, not required.simon.vergauwen
08/06/2025, 8:20 AMmbonnin
08/06/2025, 8:21 AMsimon.vergauwen
08/06/2025, 8:22 AMJoel Denke
08/06/2025, 8:22 AMSLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See <https://www.slf4j.org/codes.html#noProviders> for further details.
Exception in thread "main" java.lang.IllegalArgumentException: Neither port nor sslPort specified. Use command line options -port/-sslPort or configure connectors in application.conf
at io.ktor.server.engine.CommandLineKt.CommandLineConfig(CommandLine.kt:74)
at io.ktor.server.netty.EngineMain.createServer(EngineMain.kt:39)
at io.ktor.server.netty.EngineMain.main(EngineMain.kt:24)
at se.coody.server.ApplicationKt.main(Application.kt:13)
Not sure why I event need slf4j but ok πmbonnin
08/06/2025, 8:23 AMJoel Denke
08/06/2025, 8:24 AMmbonnin
08/06/2025, 8:25 AMmbonnin
08/06/2025, 8:25 AMmbonnin
08/06/2025, 8:26 AMmbonnin
08/06/2025, 8:26 AMktor
"starters", like there is in spring bootsimon.vergauwen
08/06/2025, 8:26 AMJoel Denke
08/06/2025, 8:26 AMJoel Denke
08/06/2025, 8:27 AMJoel Denke
08/06/2025, 8:28 AMapplication {
mainClass = "io.ktor.server.netty.EngineMain"
}
Which is nice, to streamline all config files intro one place, neat.mbonnin
08/06/2025, 8:29 AMstart.ktor.ioThat's useful but no substitute for a "battery-included" single maven coordinate IMO. Something like for "I want a simple JVM server that logs to stdout" (basically
ktor-starter-jvm
= ktor-server-netty
+ slf4j-simple
)mbonnin
08/06/2025, 8:29 AMJoel Denke
08/06/2025, 8:29 AMmbonnin
08/06/2025, 8:29 AMmbonnin
08/06/2025, 8:30 AMktor-starter-jvmI would also probably include
cors
in there. maybe content-negociation
, but not 100% sure about that one)Joel Denke
08/06/2025, 8:30 AMJoel Denke
08/06/2025, 8:31 AMprivate fun Application.configure() {
install(ContentNegotiation) { json() }
install(DefaultHeaders)
install(CORS) {
// TODO: replace by correct domain(s) in production.
allowHost("localhost:8000")
allowMethod(HttpMethod.Get)
allowMethod(<http://HttpMethod.Post|HttpMethod.Post>)
allowMethod(HttpMethod.Options)
allowHeader(HttpHeaders.ContentType)
allowHeader(HttpHeaders.Accept)
allowHeader(HttpHeaders.Origin)
allowHeader(HttpHeaders.Referrer)
allowHeader("SESSION")
exposeHeader("SESSION")
}
}
πJoel Denke
08/06/2025, 8:32 AMJoel Denke
08/06/2025, 8:36 AMJoel Denke
08/06/2025, 9:01 AMexternalRegistry = DockerImageRegistry.googleContainerRegistry(
projectName = provider { "name" },
appName = provider { project.name },
username = providers.environmentVariable("GCLOUD_REGISTRY_USERNAME"),
password = providers.environmentVariable("GCLOUD_REGISTRY_PASSWORD"),
)
Not sure what USERNAME and PASSWORD to use, usually not the way of using GCP so to say, often OAuth token?Joel Denke
08/06/2025, 9:02 AMexport GCLOUD_REGISTRY_USERNAME="oauth2accesstoken"
export GCLOUD_REGISTRY_PASSWORD=$(gcloud auth print-access-token)
For the easy local way of deploy. Ofc not the way of doing it longterm, then will probbly do Github Actions with Federated Identity and such.mbonnin
08/06/2025, 9:26 AMmbonnin
08/06/2025, 9:28 AMNot sure what USERNAME and PASSWORD to useI'm on the same boat. This is the part I always struggle with
Joel Denke
08/06/2025, 9:31 AMmbonnin
08/06/2025, 9:31 AM"_json_key"
and a service account jsonmbonnin
08/06/2025, 9:31 AMmbonnin
08/06/2025, 9:32 AM_json_key
stuff by reverse engineering the GCloud Java SDKs or something like thisJoel Denke
08/06/2025, 9:32 AMmbonnin
08/06/2025, 9:32 AMJoel Denke
08/06/2025, 9:33 AMJoel Denke
08/06/2025, 9:33 AMsimon.vergauwen
08/06/2025, 9:34 AMauth {
username = 'oauth2accesstoken'
password = 'gcloud auth print-access-token'.execute().text.trim()
}
Joel Denke
08/06/2025, 9:54 AMDockerImageRegistry.googleContainerRegistry(
projectName = provider { "API" },
appName = provider { project.name },
username = provider { "oauth2accesstoken" },
password = provider {
val byteOut = ByteArrayOutputStream()
project.providers.exec {
commandLine("gcloud", "auth", "print-access-token")
standardOutput = byteOut
}
String(byteOut.toByteArray()).trim()
}
)
So something like this then? πmbonnin
08/06/2025, 9:58 AMproject.providers.exec {}
sounds like you had a GCP problem and now you have a Gradle problem π
Joel Denke
08/06/2025, 9:58 AM* What went wrong:
Execution failed for task ':server:jib'.
> Could not create provider for value source ProcessOutputValueSource.
Probably related π§mbonnin
08/06/2025, 9:59 AMJavaExec
tasks nowadays. It's more code but at least it's somewhat consistentJoel Denke
08/06/2025, 10:00 AMfun String.runCommand(currentWorkingDir: File = file("./")): String {
val byteOut = ByteArrayOutputStream()
project.providers.exec {
workingDir = currentWorkingDir
commandLine = this@runCommand.split("\\s".toRegex())
standardOutput = byteOut
}
return String(byteOut.toByteArray()).trim()
}
mbonnin
08/06/2025, 10:01 AMabstract class GetAccessToken: DefaultTask() {
@get:OutputFile
val token: RegularFileProperty
@TaskAction
fun taskAction() {
ProcessBuilder()
. // do the regular Java stuff to start a process and retrieve stdout here
}
}
mbonnin
08/06/2025, 10:01 AMpassword = getAccessTokenTaskProvider.flatMap { it.token }
mbonnin
08/06/2025, 10:02 AMmbonnin
08/06/2025, 10:02 AMProcessBuilder
Joel Denke
08/06/2025, 10:02 AMmbonnin
08/06/2025, 10:03 AMJoel Denke
08/06/2025, 10:03 AMmbonnin
08/06/2025, 10:04 AMProcessBuilder
πJoel Denke
08/06/2025, 10:05 AMJoel Denke
08/06/2025, 10:09 AMExecution failed for task ':server:jib'.
> com.google.cloud.tools.jib.plugins.common.BuildStepsExecutionException: Build image failed, perhaps you should make sure your credentials for '<http://gcr.io/project/server|gcr.io/project/server>' are set up correctly. See <https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#what-should-i-do-when-the-registry-responds-with-unauthorized> for help
Now I seems to get here at least, I guess i just need to enable some GCP stuff first πJoel Denke
08/06/2025, 10:43 AMJoel Denke
08/06/2025, 10:44 AMfun main(args: Array<String>) {
embeddedServer(Netty, port = System.getenv("POST")?.toIntOrNull() ?: 8080) {
module()
}.start(wait = true)
}
fun Application.module() {
configure()
apolloModule(ServiceExecutableSchemaBuilder().build())
apolloSandboxModule()
}
That was finally one part being easy and setup sample doc was very good πJoel Denke
08/06/2025, 10:45 AMmbonnin
08/06/2025, 10:45 AMJoel Denke
08/06/2025, 10:46 AMJoel Denke
08/06/2025, 10:46 AMJoel Denke
08/06/2025, 10:47 AMmbonnin
08/06/2025, 10:49 AMmbonnin
08/06/2025, 10:50 AMjib
directlymbonnin
08/06/2025, 10:51 AMJoel Denke
08/06/2025, 10:51 AMJoel Denke
08/06/2025, 10:52 AMsimon.vergauwen
08/06/2025, 10:52 AMjib
directly
That might be out-of-date, and JB host KotlinConf-app on their own infra afaik (or through their own infra).Joel Denke
08/06/2025, 10:53 AMsimon.vergauwen
08/06/2025, 10:53 AMReally insane amount of stuff can do which is hidden from documentation surface.Common problem in OSS though... π
Joel Denke
08/06/2025, 10:54 AM> Task :server:run
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See <https://www.slf4j.org/codes.html#noProviders> for further details.
<===========--> 85% EXECUTING [11m 12s]
> IDLE
> IDLE
> IDLE
> IDLE
> IDLE
> :server:run
> IDLE
Also this quite confusing, it says executing in Gradle but it actually running and then it doesnt print localhost:8080 up πmbonnin
08/06/2025, 10:54 AMmbonnin
08/06/2025, 10:55 AMorg.slf4j:slf4j-simple:2.0.17
to your classpath, it will show you "listening on ..."Joel Denke
08/06/2025, 10:55 AMmbonnin
08/06/2025, 10:55 AMmbonnin
08/06/2025, 10:55 AMmbonnin
08/06/2025, 10:55 AMJoel Denke
08/06/2025, 10:56 AM<===========--> 85% EXECUTING [13m 19s]
This counting up all the time, which is weird πJoel Denke
08/06/2025, 10:56 AMmbonnin
08/06/2025, 10:56 AMmbonnin
08/06/2025, 10:57 AMmbonnin
08/06/2025, 10:58 AMmbonnin
08/06/2025, 10:58 AMmbonnin
08/06/2025, 10:59 AMmbonnin
08/06/2025, 11:00 AMmbonnin
08/06/2025, 11:00 AMmbonnin
08/06/2025, 11:02 AMJoel Denke
08/06/2025, 12:17 PMJoel Denke
08/06/2025, 12:17 PMJoel Denke
08/06/2025, 12:20 PMJoel Denke
08/06/2025, 3:11 PMmbonnin
08/06/2025, 3:15 PMmbonnin
08/06/2025, 3:16 PMJoel Denke
08/06/2025, 3:16 PMJoel Denke
08/06/2025, 3:16 PMmbonnin
08/06/2025, 3:16 PMmbonnin
08/06/2025, 3:16 PMmbonnin
08/06/2025, 3:17 PMJoel Denke
08/06/2025, 3:17 PMmbonnin
08/06/2025, 3:17 PMmbonnin
08/06/2025, 3:18 PMmbonnin
08/06/2025, 3:18 PMJoel Denke
08/06/2025, 3:18 PMmbonnin
08/06/2025, 3:19 PMJoel Denke
08/06/2025, 3:19 PMmbonnin
08/06/2025, 3:20 PMJoel Denke
08/06/2025, 3:20 PMJoel Denke
08/06/2025, 3:20 PMJoel Denke
08/06/2025, 3:23 PMtasks.register<Exec>("deployToCloudRun") {
group = "cloud run"
description = "Deploys the application to Google Cloud Run."
dependsOn(tasks.named("jib"))
executable = "gcloud"
// executable = "/usr/local/google-cloud-sdk/bin/gcloud"
args = listOf(
"run", "deploy", "my-api-service",
"--image", "<http://gcr.io/project/${project.name}:${project.version}|gcr.io/project/${project.name}:${project.version}>", // Image frΓ₯n Jib
"--project", "project",
"--region", "europe-west1",
"--platform", "managed",
"--quiet",
"--allow-unauthenticated"
)
}
Looks like bare minimum is just adding this into my server module build gradle file πJoel Denke
08/07/2025, 6:21 AMmbonnin
08/07/2025, 6:54 AMJoel Denke
08/07/2025, 6:57 AMJoel Denke
08/07/2025, 7:20 AMJoel Denke
08/07/2025, 7:22 AMJoel Denke
08/07/2025, 7:23 AMmbonnin
08/07/2025, 7:48 AMJoel Denke
08/07/2025, 7:50 AMmbonnin
08/07/2025, 7:53 AMGcloud CLI is nice, but its very complex what parameters should use and when, its then most oten nice having default values in a plugin with best practice values for most users.I'm not a fan of this trend to ship CLI binaries instead of libs. It makes everything type unsafe, requires parsing output, etc... Plus we have to find another way to keep the versions up to date, etc.... </rant>
Joel Denke
08/07/2025, 7:55 AMmbonnin
08/07/2025, 7:57 AMJoel Denke
08/07/2025, 7:59 AM